#include "video_memory.h"
#include "nes.h"

#include <malloc.h>
#include <string.h>

/*
  from InfoNES

  Palette Entries
    Bit7-6  Not used    (contains garbage when reading palette memory)
    Bit5-4  Luminance   (Grayscale) (0-3)
    Bit3-0  Chrominance (Color)     (0-F)
*/
static u16 s_NesPalette[] = {
  0x39ce, 0x1071, 0x0015, 0x2013, 0x440e, 0x5402, 0x5000, 0x3c20,
  0x20a0, 0x0100, 0x0140, 0x00e2, 0x0ceb, 0x0000, 0x0000, 0x0000,
  0x5ef7, 0x01dd, 0x10fd, 0x401e, 0x5c17, 0x700b, 0x6ca0, 0x6521,
  0x45c0, 0x0240, 0x02a0, 0x0247, 0x0211, 0x0000, 0x0000, 0x0000,
  0x7fff, 0x1eff, 0x2e5f, 0x223f, 0x79ff, 0x7dd6, 0x7dcc, 0x7e67,
  0x7ae7, 0x4342, 0x2769, 0x2ff3, 0x03bb, 0x0000, 0x0000, 0x0000,
  0x7fff, 0x579f, 0x635f, 0x6b3f, 0x7f1f, 0x7f1b, 0x7ef6, 0x7f75,
  0x7f94, 0x73f4, 0x57d7, 0x5bf9, 0x4ffe, 0x0000, 0x0000, 0x0000
};

CVideoMemory::CVideoMemory(CNes* nes) : m_Nes(nes)
{
  m_FinalFrameBuf = new u16[SCREEN_W*SCREEN_H + 0x10];
  m_FrameBuf      = new u16[SCREEN_W*SCREEN_H + 0x10];
  m_PatternImage  = new u16[PATTERN_W*PATTERN_H + 0x10];
  m_NameTblImage  = new u16[NAME_TBL_W*2*NAME_TBL_H*2 + 0x10];
  m_SpriteImage   = new u16[SCREEN_W*SCREEN_H + 0x10];
}

CVideoMemory::~CVideoMemory()
{
  delete m_SpriteImage;
  delete m_NameTblImage;
  delete m_PatternImage;
  delete m_FrameBuf;
  delete m_FinalFrameBuf;
}

void
CVideoMemory::Reset()
{
  memset(m_FinalFrameBuf, 0, SCREEN_W*SCREEN_H*2);
  memset(m_FrameBuf, 0, SCREEN_W*SCREEN_H*2);
  memset(m_PatternImage, 0, PATTERN_W*PATTERN_H*2);
  memset(m_NameTblImage, 0, NAME_TBL_W*2*NAME_TBL_H*2*2);
  memset(m_SpriteImage, 0, SCREEN_W*SCREEN_H*2);
}

void
CVideoMemory::ClearScreen()
{
  memset(m_FinalFrameBuf, 0, SCREEN_W*SCREEN_H*2);
}

void
CVideoMemory::BuildTiles()
{
  u8* ptn_tbl = m_Nes->GetPatternTbl();
  for (int i = 0; i < TILES_COUNT_IN_PTN_TBL; i++) {
    PPUTile& tile= m_BGPattern[i];
    for (int y = 0; y < 8; y++) {
      u8 color0 = ptn_tbl[i*0x10+y+0];
      u8 color1 = ptn_tbl[i*0x10+y+8];
      for (int x = 0; x < 8; x++) {
        tile[y][x]  = (((color0>>(8-x-1))&1));
        tile[y][x] |= (((color1>>(8-x-1))&1)<<1);
      }
    }
  }
}

void
CVideoMemory::BuildPatternTbl()
{
  BuildTiles();

  u8* pal_tbl = m_Nes->GetBGPalette();
  for (int h = 0; h < PATTERN_H; h++) {
    for (int w = 0; w < PATTERN_W; w++) {
      int delta_x = w / 8, delta_y = h / 8;
      PPUTile& tile= m_BGPattern[delta_x + delta_y*16];
      int ofs_x = w % 8, ofs_y = h % 8;

      int pal_index = tile[ofs_y][ofs_x]&0xF;
      u8  pal = pal_tbl[pal_index];
      m_PatternImage[w+h*PATTERN_W] = s_NesPalette[pal];
    }
  }
}

void
CVideoMemory::BuildNameTbl()
{
  BuildTiles();

  u8* pal_tbl = m_Nes->GetBGPalette();
  bool use_ptn_tbl_0 = ((g_NES->m_Ppu->Register[0]&PPU_NAME_PTN_TBL) == 0);
  int tile_index_0 = use_ptn_tbl_0?0:(TILES_COUNT_IN_PTN_TBL/2);
  for (int i = 0; i < 4; i++) {
    u8* name_tbl = m_Nes->GetNameTbl(i);
    u8* attr_tbl = m_Nes->GetAttribTbl(i);

    u16* cur_block = m_NameTblImage +
      ((i>>1)*NAME_TBL_W*2*NAME_TBL_H + (i&1)*NAME_TBL_W);

    for (int h = 0; h < TILE_COUNT_H; h++) {
      for (int w = 0; w < TILE_COUNT_W; w++) {
        int attrib_index  = (h/4)*(TILE_COUNT_W/4) + w/4;
        u8 attrib = attr_tbl[attrib_index];

        int tile_index    = h*TILE_COUNT_W + w;
        int pattern_index = name_tbl[tile_index&0x3FF];

        // Get current tile info
        PPUTile tile;
        memcpy(&tile, &m_BGPattern[pattern_index+tile_index_0], sizeof(tile));

        // Get current attribute info
        /*
          Bit0-1  Palette Number for upperleft 16x16 pixels of the 32x32 area
          Bit2-3  Palette Number for upperright 16x16 pixels of the 32x32 area
          Bit4-5  Palette Number for lowerleft 16x16 pixels of the 32x32 area
          Bit6-7  Palette Number for lowerright 16x16 pixels of the 32x32 area
        */
        int x = tile_index % TILE_COUNT_W % 4;
        int y = tile_index / TILE_COUNT_W % 4;
        int ofs = ((x>>1)+(y&2))*2;
        attrib = (((attrib >> ofs) & 3) << 2);

        u16* tile_block = &cur_block[(w + h*NAME_TBL_W*2) * 8];
        for (int y = 0; y < 8; y++) {
          for (int x = 0; x < 8; x++) {
            // need palette data here
            int pal_index = (tile[y][x] == 0)?0:(attrib | tile[y][x])&0xF;
            u8  pal = pal_tbl[pal_index];
            tile_block[x] = s_NesPalette[pal];
          }
          tile_block += (NAME_TBL_W*2);
        }
      }
    }
  }
}

void
CVideoMemory::BuildFrame()
{
  BuildNameTbl();

  int x = m_Nes->m_Ppu->m_OfsX;
  int y = m_Nes->m_Ppu->m_OfsY;

  int total_w = NAME_TBL_W*2;
  int total_h = NAME_TBL_H*2;

  if (x >= total_w) {
    x = 0;
  }
  if (y >= total_h) {
    y = 0;
  }

  int cur_w = total_w - x;
  int rst_w = x - total_w/2;
  int cur_h = y + SCREEN_H;
  int rst_h = y - total_h/2;
  if (cur_h > total_h) {
    cur_h = total_h - 1;
  }

  // 1
  int j = 0;
  for (int i = y; i < cur_h; i++, j++) {
    u16* src_buf = m_NameTblImage + i*total_w;
    u16* src_part_1 = src_buf + x;
    u16* dst_part_1 = m_FrameBuf + j*SCREEN_W;
    if (cur_w > SCREEN_W) {
      cur_w = SCREEN_W - 1;
    }
    memcpy(dst_part_1, src_part_1, cur_w*2);

    if (rst_w > 0) {
      u16* src_part_2 = src_buf;
      u16* dst_part_2 = dst_part_1 + cur_w;
      memcpy(dst_part_2, src_part_2, rst_w*2);
    }
  }
  // rest
  for (int i = 0; i < rst_h && j < SCREEN_H; i++, j++) {
    u16* src_buf = m_NameTblImage + i*total_w;
    u16* src_part_1 = src_buf + x;
    u16* dst_part_1 = m_FrameBuf + j*SCREEN_W;
    if (cur_w > SCREEN_W) {
      cur_w = SCREEN_W - 1;
    }
    memcpy(dst_part_1, src_part_1, cur_w*2);

    if (rst_w > 0) {
      u16* src_part_2 = src_buf;
      u16* dst_part_2 = dst_part_1 + cur_w;
      memcpy(dst_part_2, src_part_2, rst_w*2);
    }
  }
}

void
CVideoMemory::BuildSprites()
{
  BuildTiles();

  memset(m_SpriteImage, 0, SCREEN_W*SCREEN_H*2);

  u8* pal_tbl = m_Nes->GetSPPalette();

  SpriteInfo* sprites = (SpriteInfo*)m_Nes->m_Ppu->SprRam;
  bool is_8_8 = ((m_Nes->m_Ppu->Register[0]&PPU_SP_SIZE) == 0);
  int spr_bank = (((m_Nes->m_Ppu->Register[0]&PPU_SP_PTN_TBL) == 0)?0:1);
  for (int i = MAX_SPRITE_NUM-1; i >= 0; i--) {
    SpriteInfo& sprite = sprites[i];
    if (sprite.y > 0xEE) {
      continue;
    }

    int tile_index = (is_8_8?sprite.tile:(sprite.tile&0xFE));
    PPUTile& tile0 = m_BGPattern[spr_bank*TILES_COUNT_IN_PTN_TBL/2+tile_index];
    u16* sp_buf = m_SpriteImage + (sprite.y + 1)*SCREEN_W + sprite.x;
    for (int x = 0; x < 8; x++) {
      for (int y = 0; y < 8; y++) {
        int real_x = sprite.attrib&SP_ATTRIB_FLIP_X?(8-x-1):x;
        int real_y = sprite.attrib&SP_ATTRIB_FLIP_Y?(8-y-1):y;
        int pal_index = (tile0[real_y][real_x]&0xF) | ((sprite.attrib&3)<<2);
        u8  pal = pal_tbl[pal_index];
        sp_buf[x + y*SCREEN_W] = s_NesPalette[pal];
      }
    }
    if (is_8_8) {
      continue;
    }
    PPUTile& tile1 = m_BGPattern[spr_bank*TILES_COUNT_IN_PTN_TBL/2+tile_index+1];
    sp_buf = m_SpriteImage + (sprite.y + 1 + 8)*SCREEN_W + sprite.x;
    for (int x = 0; x < 8; x++) {
      for (int y = 0; y < 8; y++) {
        int real_x = sprite.attrib&SP_ATTRIB_FLIP_X?(8-x-1):x;
        int real_y = sprite.attrib&SP_ATTRIB_FLIP_Y?(8-y-1):y;
        int pal_index = (tile1[real_y][real_x]&0xF) | ((sprite.attrib&3)<<2);
        u8  pal = pal_tbl[pal_index];
        sp_buf[x + y*SCREEN_W] = s_NesPalette[pal];
      }
    }
  }
}

void
CVideoMemory::DrawScanLine(int scanline)
{
  DrawBG(scanline);
  DrawSprite(scanline);
}

void
CVideoMemory::DrawBG(int scanline)
{
  u16* line_buf = m_FinalFrameBuf + scanline * SCREEN_W;
  if ((m_Nes->m_Ppu->Register[1]&PPU_SHOW_BG) == 0) {
    memset(line_buf, 0, SCREEN_W * 2);
    return;
  }

  int x = m_Nes->m_Ppu->m_OfsX;
  int y = m_Nes->m_Ppu->m_OfsY + 1 + scanline;

  int r2000_name_tbl = g_NES->m_Ppu->Register[0]&PPU_NAMETABLE;
  if (y > NAME_TBL_H) {
    r2000_name_tbl ^= 2;
    y -= NAME_TBL_H;
  }
  int delta_y = y&7;
  y >>= 3;
  int ofs_y = y%TILE_COUNT_H;

  int total_w = NAME_TBL_W*2;
  int total_h = NAME_TBL_H*2;

  int right_x = x + NAME_TBL_W;
  if (right_x > total_w) {
    right_x = total_w;
  }
  int left_x = x - NAME_TBL_W;

  u8* pal_tbl = m_Nes->GetBGPalette();
  bool use_ptn_tbl_0 = ((g_NES->m_Ppu->Register[0]&PPU_NAME_PTN_TBL) == 0);
  u8* ptn_tbl = m_Nes->GetPatternTbl(use_ptn_tbl_0?0:1);

  bool need_exit = false;
  int j = 0;
DrawIt:
  for (int i = x; i < right_x; i++, j++) {
    // xor is great! maybe I should re-read CSAPP
    int cur_name_tbl = (i/NAME_TBL_W)?(r2000_name_tbl^1):r2000_name_tbl;
    cur_name_tbl &= 3;
    u8* name_tbl = m_Nes->GetNameTbl(cur_name_tbl);
    u8* attr_tbl = m_Nes->GetAttribTbl(cur_name_tbl);

    // find the tile
    int ofs_x = ((i % NAME_TBL_W) >> 3);
    int delta_x = (i&7);

    // pattern tbl
    int tile_index = ofs_y * TILE_COUNT_W + ofs_x;
    int pattern_index = name_tbl[tile_index&0x3FF];
    u8 color0 = ptn_tbl[pattern_index*0x10+delta_y+0];
    u8 color1 = ptn_tbl[pattern_index*0x10+delta_y+8];

    u8 color = (((color0>>(8-delta_x-1))&1));
    color |= (((color1>>(8-delta_x-1))&1)<<1);

    // attribute tbl
    int attrib_index  = (ofs_y >> 2)*(TILE_COUNT_W >> 2) + (ofs_x >> 2);
    u8 attrib = attr_tbl[attrib_index];

    int tmp_x = tile_index % TILE_COUNT_W % 4;
    int tmp_y = tile_index / TILE_COUNT_W % 4;
    int ofs = ((tmp_x>>1)+(tmp_y&2))*2;
    attrib = (((attrib >> ofs) & 3) << 2);

    int pal_index = (color == 0)?0:(attrib | color)&0xF;
    u8  pal = pal_tbl[pal_index];
    line_buf[j] = s_NesPalette[pal];
  }

  if (need_exit) {
    return;
  }

  // draw the rest
  x = 0; right_x = left_x;
  need_exit = true;
  goto DrawIt;
}

// Sprite 0 hit here
void
CVideoMemory::DrawSprite(int scanline)
{
  if (!(m_Nes->m_Ppu->Register[1] & PPU_SHOW_SP)) {
    return;
  }

  int sp_count = 0;

  u8* pal_tbl = m_Nes->GetSPPalette();

  SpriteInfo* sprites = (SpriteInfo*)m_Nes->m_Ppu->SprRam;
  bool is_8_8 = ((m_Nes->m_Ppu->Register[0]&PPU_SP_SIZE) == 0);
  int spr_height = is_8_8 ? 8 : 16;
  int spr_bank = (((m_Nes->m_Ppu->Register[0]&PPU_SP_PTN_TBL) == 0)?0:1);
  u8* ptn_tbl = m_Nes->GetPatternTbl(spr_bank);
  for (int i = MAX_SPRITE_NUM-1; i >= 0; i--) {
    SpriteInfo& sprite = sprites[i];
    int y = sprite.y + 1;
    if (y > SCREEN_H-1) {
      continue;
    }

    if (y > scanline || y + spr_height <= scanline) {
      continue;
    }

    sp_count++;
    if (sp_count >= 8) {
      m_Nes->m_Ppu->Register[2] |= PPU_MAX_SPRITE;
    }

    int delta_y = scanline - y;
    int pattern_ofs = (delta_y >> 3);
    delta_y &= 7;
    delta_y = (sprite.attrib&SP_ATTRIB_FLIP_Y) ? (8-delta_y-1) : delta_y;

    int pattern_index =
      (is_8_8 ? sprite.tile : ((int)(sprite.tile&0xFE) + pattern_ofs));
    u8 color0 = ptn_tbl[pattern_index*0x10+delta_y+0];
    u8 color1 = ptn_tbl[pattern_index*0x10+delta_y+8];

    u16* sp_buf = m_FinalFrameBuf + scanline*SCREEN_W + sprite.x;
    for (int i = 0; i < 8; i++) {
      int delta_x = (sprite.attrib&SP_ATTRIB_FLIP_X) ? (8-i-1) : i;

      u8 color = (((color0>>(8-delta_x-1))&1));
      color |= (((color1>>(8-delta_x-1)<<1)&2));

      int pal_index = color | ((sprite.attrib&3) << 2);
      u8  pal = pal_tbl[pal_index];
      if (color != 0) {
        g_NES->m_Ppu->Register[2] |= PPU_SPRITE_0_HIT;
      }
      if ((pal_index & 3)) {
        sp_buf[i] = s_NesPalette[pal];
      }
    }
  }
}
